Pro ASP.NET Core MVC2(第7版)翻译

第16章:高级路由功能

作者:Adam Freeman 翻译:陈广 日期:2018-9-22


在上一章中,我向您展示了如何使用路由系统来处理传入 URL,但这只是故事的一部分。还需要能够使用您的 URL 架构来生成传出 URL,您可以将其嵌入到视图中,这样用户就可以单击链接并将表单提交回您的应用程序,从而以正确的控制器和 action 为目标。

本章我将向您展示生成传出 URL 的不同技术,如何通过替换标准的 MVC 路由实现类来定制路由系统,以及如何使用 MVC 区域 特性,它允许您将一个庞大而复杂的 MVC 应用程序分解为可管理的块。在完成本章时,我将提供一些关于 MVC 应用程序中 URL 架构的最佳实践建议。表16-1为高级路由特性简介。

表 16-1:高级路由特性简介

问题 回答
它是什么? 路由系统提供的功能不仅仅是匹配 HTTP 请求的 URL,还支持在视图中生成 URL,用自定义类替换内置路由功能,并将应用程序构造为独立的部分。
它有什么用? 每个特性都有不同的用途。能够生成 URL 并可以轻松地更改 URL 架构,而无需更新所有视图;能够使用自定义类来根据您的需要定制路由系统;能够构造应用程序以便更容易地构建复杂的项目。
它是如何使用的? 查看本章相关章节获取详细信息
它有什么缺陷和限制? 复杂应用程序的路由配置很难管理。
有没有其它选择? 没有,路由系统是 ASP.NET 的一个组成部分。

表 16-2 是本章摘要

表 16-2:本章摘要

问题 解决方案 清单
生成带有 URL 的锚元素 使用asp-actionasp-controller属性 2-5
为路由段提供值 使用带asp-route-前缀的属性 6-7
生成完全限定 URL 使用asp-procotolasp-hostasp-fragmen属性 8
选择生成 URL 的路由 使用asp-route属性 9-10
生成一个没有 HTML 元素的 URL 在视图或 action 方法中使用Url.Action助手方法 11-12
定制路由系统 Startup类中使用Configure方法 13
创建自定义路由类 实现IRouter接口 14-21
将应用程序分解为功能部分 创建区域并使用Area特性 22-28

准备示例项目

我准备继续使用上一章的 UrlsAndRoutes 项目。所需的唯一更改是在Startup类中,我用具有相同效果的显式路由替换了UseMvcWithDefaultRoute方法,如清单16-1所示。

清单 16-1:UrlsAndRoutes 文件夹下的 Startup.cs 文件,更改路由配置

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

如果启动应用程序,浏览器将请求默认 URL,该 URL 将被发送到 Home 控制器的Index action,如图16-1所示。

图16-1 运行示例应用程序

在视图中生成传出 URL

在几乎所有 MVC 应用程序中,您都希望允许用户从一个视图导航到另一个视图,这通常依赖于在第一个视图中包含一个链接,该链接的目标是生成第二个视图的 action 方法。很容易添加一个静态元素(称为锚元素),其href属性以 action 方法为目标,如下所示:

<a href="/Home/CustomVariable">This is an outgoing URL</a>

假设应用程序正在使用默认的路由配置,这个 HTML 元素将创建一个链接,该链接将针对 Home 控制器上的CustomVariable action 方法。象这样手动定义的 URL 是非常快速而简单的。它们也非常危险,当您更改应用程序的 URL 架构时,将破坏所有硬编码的 URL。然后,您必须遍历应用程序中的所有视图,并更新对控制器和 action 方法的所有引用,这是一个繁琐、容易出错和难以测试的过程。另一种更好的选择是使用路由系统生成传出 URL,这将确保 URL 架构用于动态生成 URL,并以保证反映应用程序的 URL 架构的方式进行。

生成传出链接

在视图中生成传出 URL 的最简单方法是使用 anchor 标签助手,它将为 HTML a 元素生成href属性,如清单16-2所示,清单16-2显示了我对 /Views/Shared/Result.cshtml 视图的添加。

提示:我在第23章详细解释了标签助手是如何工作的。

清单 16-2:Views/Shared 文件夹下的 Result.cshtml 文件,使用 Anchor 标签助手

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-action="CustomVariable">This is an outgoing URL</a>
</body>
</html>

asp-action属性用于指定href属性中 URL 应该指向的 action 方法的名称。您可以通过启动应用程序查看结果,如图16-2所示。

图16-2 使用标签助手生成链接

标记助手使用当前的路由配置在 a 元素上设置href属性。如果检查发送到浏览器的 HTML,您将看到它包含以下元素:

<a href="/Home/CustomVariable">This is an outgoing URL</a>

这似乎是重新创建我前面向您展示的手动定义的 URL,但是这种方法的好处是它会自动响应路由配置中的更改。为了演示,我向 Startup.cs 文件添加了一个新的路由,如清单16-3所示。

清单 16-3:UrlsAndRoutes 文件夹下的 Startup.cs 文件,添加路由

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "NewRoute",
                    template: "App/Do{action}",
                    defaults: new { controller = "Home" });

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

新路由更改了针对 Home 控制器的请求的 URL 架构。如果启动应用程序,您将看到此更改反映在ActionLink HTML 助手方法生成的 HTML 中,如下所示:

<a href="/App/DoCustomVariable">This is an outgoing URL</a>

使用标签助手生成链接解决了一个重要的维护问题。我能够更改路由架构,并使视图中的传出链接自动反映更改,而不必手动编辑应用程序中的视图。

单击该链接时,将使用传出 URL 创建传入 HTTP 请求,然后使用相同的路由来针对将处理请求的 action 方法和控制器,如图16-3所示。

图16-3 单击链接的效果是将传出 URL 转换为传入请求


理解出站 URL 路由匹配

您已经看到,更改定义 URL 架构的路由是如何改变生成传出 URL 的方式的。应用程序通常会定义几个路由,了解如何选择它们来生成 URL 是很重要的。路由系统按照定义的顺序处理路由,并依次检查每个路由是否匹配,这要求满足以下三个条件:

  • 必须为 URL 模式中定义的每个段变量提供一个值。要查找每个段变量的值,路由系统首先查找您提供的值(使用匿名类型的属性),然后查找当前请求的变量值,最后查看路由中定义的默认值。(我将在本章稍后返回这些值的第二个源)。
  • 为段变量提供的任何值都不可能与路由中定义的默认惟一变量不一致。这些变量提供了默认值但不在 URL 模式中出现。例如,在以下路由定义中,myVar是一个默认惟一变量:
routes.MapRoute("MyRoute", "{controller}/{action}",
    new { myVar = "true" });

为了使这个路由匹配,我必须注意不要为myvar提供一个值,或者确保我所提供的值与默认值相匹配。

  • 所有段变量的值必须满足路由约束。请参阅前一章中的《约束路由》一节,以获得不同类型约束的示例。

要明确的是,路由系统并不试图找到提供最佳匹配的路由,它只找到了第一个匹配点,此时它使用该路由生成 URL;任何后续路由被将被忽略。由于这个原因,你应该首先定义最具体的路由。检查传出 URL 生成非常重要。如果您试图生成一个无法找到匹配路由的 URL,将会创建一个包含空href属性的链接,如下所示:

<a href="">This is an outgoing URL</a>`

该链接将在视图中正确渲染,但当用户单击该链接时将无法正常运行。如果只生成 URL(我将在本章稍后向您展示),则结果将为null,它将渲染为视图中的空字符串。您可以通过使用命名路由来控制路由匹配。有关详细信息,请参阅本章后面的《从指定路由生成 URL》一节。


针对其它控制器

当您在元素上指定asp-action属性时,标签助手假定您希望所针的 action 在导致视图渲染的那个控制器中。要创建针对不同控制器的传出 URL,可以使用 asp-controller 属性,如清单16-4所示。

清单 16-4:Views/Shared 文件夹下的 Result.cshtml 文件,针对不同的控制器

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Admin" asp-action="Index">
        This targets another controller
    </a>
</body>
</html>

当您渲染视图,将会看到生成以下 HTML:

<a href="/Admin">This targets another controller</a>

标签助手将针对 Admin 控制器上Index action 方法的 URL 请求表示为 /Admin。路由系统知道,应用程序中定义的路由默认使用Index action 方法,允许它省略不需要的段。

在决定如何针对给定的 action 方法时,路由系统会包含使用Route特性定义的路由。在清单16-5中,asp-controller属性的目标是 Customer 控制器中的Index action,在第15章中已经应用了Route特性。

清单 16-5:Views/Shared 文件夹下的 Result.cshtml 文件,针对一个 Action

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Customer" asp-action="Index">This is an outgoing URL</a>
</body>
</html>

链接生成如下 HTML:

<a href="/app/Customer/actions/Index">This is an outgoing URL</a>

在15章中我针对 Customer 控制器应用的Route特性为:

...
[Route("app/[controller]/actions/[action]/{id:weekday?}")]
public class CustomerController : Controller {
...

传递额外值

您可以将段变量的值传递给路由系统,方法是定义其名称以asp-route-开头的属性,后面跟着段名,以使asp-route-id用于设置id段的值,如清单16-6所示。

清单 16-6:Views/Shared 文件夹下的 Result.cshtml 文件,为段变量提供值

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Home" asp-action="Index" asp-route-id="Hello">
        This is an outgoing URL
    </a>
</body>
</html>

我为一个名为id的段变量提供了一个值。如果应用程序使用清单16-6所示的路由,那么视图中将呈现以下 HTML:

<a href="/App/DoIndex?id=Hello">This is an outgoing URL</a>

注意,段值已作为查询字符串的一部分添加,以适应路由描述的 URL 模式。这是因为在那个路由中没有对应于id的段变量。为了解决这个问题,我在 Startup.cs 文件中编辑了路由,只留下一个有id段的路由,如清单16-7所示。

清单 16-7:UrlsAndRoutes 文件夹下的 Startup.cs 文件,编辑路由

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                //routes.MapRoute(
                //    name: "NewRoute",
                //    template: "App/Do{action}",
                //    defaults: new { controller = "Home" });

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

再次运行应用程序,您将看到标签助手生成以下 HTML 元素,其中id属性值包含在 URL 段中:

<a href="/Home/Index/Hello">This is an outgoing URL</a>

理解段变量重用

当我描述路由与出站 URL 匹配的方式时,我解释说,当试图为路由的 URL 模式中的每个段变量找到值时,路由系统将查看来自当前请求的值。这是一种混淆许多程序员的行为,可能导致长时间的调试会话。、

假设应用程序只有一个路由,如下所示:

...
app.UseMvc(routes => {
    routes.MapRoute(name: "MyRoute",
        template: "{controller}/{action}/{color}/{page}");
});
...

现在假设一个用户当前位于URL /Home/Index/Red/100,我渲染一个链接如下:

...
<a asp-controller="Home" asp-action="Index" asp-route-page="789">
This is an outgoing URL
</a>
...

您可能认为路由系统无法匹配路由,因为我没有为color段变量提供一个值,并且没有定义默认值。但是,您可能会出错。路由系统将与我定义的路由匹配。它将生成以下 HTML:

<a href="/Home/Index/Red/789">This is an outgoing URL</a>

路由系统热衷于与路由进行匹配,以便在生成传出 URL 时重用来自传入 URL 的段变量值。本例中,我以color变量的Red值结束,因为我的假想用户是从这个 URL 开始的。

这不是最终的行为。路由系统将应用这一技术作为其对路由的定期评估的一部分,即使存在匹配的后续路由,也不需要重用当前请求中的值。

我强烈建议您不要依赖此行为,而为 URL 模式中的所有段变量提供值。依赖这种行为不仅会使您的代码更难阅读,而且您最终会对用户发出请求的顺序做出假设,这将在应用程序进入维护时最终刺痛您。


生成完全限定 URL

到目前为止生成的所有链接都包含相对 URL,但是锚元素标签助手也可以生成完全限定的 URL,如清单16-8所示。

清单 16-8:Views/Shared 文件夹下的 Result.cshtml 文件,生成完全限定 URL

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Home" asp-action="Index" asp-route-id="Hello"
       asp-protocol="https" asp-host="myserver.mydomain.com"
       asp-fragment="myFragment">
        This is an outgoing URL
    </a>
</body>
</html>

asp-protocolasp-hostasp-fragment属性用于指定协议(清单中为 https),服务器名称(myserver.mydomain.com)和 URL 段(myFragment)。这些值与路由系统的输出相结合以创建完全限定 URL,如果运行应用程序您可以看到发送到浏览器的 HTML。

<a href="https://myserver.mydomain.com/Home/Index/Hello#myFragment">
    This is an outgoing URL
</a>

在使用完全限定 URL 时要小心,因为它们会创建对应用程序基础结构的依赖关系,当基础结构发生变化时,您必须记住对 MVC 视图进行相应的更改。

从指定路由生成 URL

在前面的示例中,路由系统选择了将用于生成 URL 的路由。如果以特定格式生成 URL 很重要,则可以指定用于生成传出 URL 的路由。为了演示它是如何工作的,我在 Startup.cs 文件中添加了一个新的路由,以便在示例应用程序中有两个路由,如清单16-9所示。

清单 16-9:UrlsAndRoutes 文件夹下的 Startup.cs 文件,添加路由

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                //routes.MapRoute(
                //    name: "NewRoute",
                //    template: "App/Do{action}",
                //    defaults: new { controller = "Home" });

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "out",
                    template: "outbound/{controller=Home}/{action=Index}");
            });
        }
    }
}

清单16-10中所示的视图包含两个锚元素,每个锚元素指定相同的控制器和 action。不同之处在于,第二个元素使用asp-route标签助手属性来指定应该使用out路由为href属性生成 URL。

清单 16-10:Views/Shared 文件夹下的 Result.cshtml 文件,生成 URL

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-controller="Home" asp-action="CustomVariable">This is an outgoing URL</a>
    <a asp-route="out">This is an outgoing URL</a>
</body>
</html>

asp-route属性只能在asp-controllerasp-action属性缺失时使用,这意味着您只能为控制器和导致视图渲染的 action 选择特定的路由。如果运行该示例并请求 /Home/CustomVariable URL,您将看到路由生成的两个不同的 URL。

<a href="/Home/CustomVariable">This is an outgoing URL</a>
<a href="/outbound">This is an outgoing URL</a>

命名路由的不利情形

依赖路由名称来生成传出URL的问题是,这样做打破了对 MVC 设计模式至关重要的关注点的分离。在视图或 action 方法中生成链接或 URL 时,我希望重点关注用户将被定向到的 action 和控制器,而不是将使用的 URL 格式。通过将不同路由的知识引入视图或控制器,我正在创建可以避免的依赖关系。在我自己的项目中,我倾向于避免命名我的路由(通过将name参数指定为null),并且倾向于使用代码注释来提醒自己每条路由要做什么。


生成 URL(无链接)

标签助手的限制是,它们转换 HTML 元素,如果您需要为应用程序生成一个 URL 而没有周围的 HTML,就不能轻易地重新使用它。

MVC 提供了一个助手类,它可以直接用于创建 URL,可以通过Url.Action方法获得,如清单16-11所示。

清单 16-11:Views/Shared 文件夹下的 Result.cshtml 文件,生成 URL

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <p>URL: @Url.Action("CustomVariable", "Home", new { id = 100 })</p>
</body>
</html>

Url.Action方法的参数指定 action 方法、控制器和任何段变量的值。清单16-11生成以下输出:

<p>URL: /Home/CustomVariable/100</p>

在 Action 方法中生成 URL

在 C# 代码中,Url.Action方法也可用于在 action 方法中创建 URL。清单16-12中,我修改了 Home 控制器的一个 action 方法,以便使用Url.Action生成一个 URL。

清单 16-12:Controllers 文件夹下的 HomeController.cs 文件,在 Action 方法中生成 URL

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(Index)
            });

        public ViewResult CustomVariable(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(CustomVariable),
            };
            r.Data["Id"] = id ?? "<no value>";
            r.Data["Url"] = Url.Action("CustomVariable", "Home", new { id = 100 });
            return View("Result", r);
        }
    }
}

如果运行该示例并请求 /Home/CustomVariable URL,您将看到表中有一行显示 URL,如图16-4所示。

图16-4 在 action 方法中生成 URL

自定义路由系统

您已经看到了路由系统的灵活和可配置性,但是如果它不符合您的要求,您可以定制行为。在本节中,我将向您展示不同的实现方法。

更改路由系统配置

在第15章中,我向您展示了如何配置 Startup.cs 文件中的RouteOptions对象来设置自定义的路由约束。RouteOptions对象也用于配置一些路由特性,使用表16-3中描述的属性。

表 16-3RouteOptions配置属性

名称 描述
AppendTrailingSlash 如果为true,则此 bool 属性将向路由系统生成的 URL 追加一个尾斜杠。默认值为false
LowercaseUrls 当控制器、action 或段值包含大写字符时,此 bool 属性将 URL 转换为小写。默认值为false

在清单16-13中,我将语句添加到 Startup.cs 文件中,以设置表16-3中描述的两个配置属性。

清单 16-13:UrlsAndRoutes 文件夹下的 Startup.cs 文件,配置路由系统

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options => {
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint));
                options.LowercaseUrls = true;
                options.AppendTrailingSlash = true;
            });
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "out",
                    template: "outbound/{controller=Home}/{action=Index}");
            });
        }
    }
}

如果运行应用程序并检查路由系统生成的 URL,您将看到更改的配置属性使 URL 全部小写,并附加了一个尾随斜杠,如图16-5所示。

图16-5 配置路由系统

创建自定义路由类

如果您不喜欢路由系统匹配 URL 的方式,或者您需要为应用程序实现特定内容,可以创建自己的路由类并使用它们来处理URL。ASP.NET 提供了Microsoft.AspNetCore.Routing.IRouter接口,它可用于实现自定义路由的创建。以下是IRouter接口的定义:

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Routing {

    public interface IRouter {

        Task RouteAsync(RouteContext context);

        VirtualPathData GetVirtualPath(VirtualPathContext context);
    }
}

要创建自定义路由,您可以实现RouteAsync异步方法来处理传入的请求,如果要生成传出 URL,则实现GetVirtualPath方法。

为了演示,我将创建一个自定义的路由类来处理遗留的 URL 请求。假设我已经将一个现有的应用程序迁移到 MVC,但有些用户已经将 pre-MVC URL 标记为书签或硬编码到脚本中。我仍然想支持那些旧的 URL。则可以使用常规的路由系统来处理这个问题,但是这个问题为本节提供了一个很好的例子。

路由传入 URL

为了理解自定义路由是如何工作的,我将首先创建一个路由来处理请求本身的各个方面,而不使用控制器和视图。我在 Infrastructure 文件夹中创建了一个名为 LegacyRoute.cs 的类文件,并使用它实现IRouter接口,如清单16-14所示。

清单 16-14:Infrastructure 文件夹下的 LegacyRoute.cs 文件的内容

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UrlsAndRoutes.Infrastructure
{
    public class LegacyRoute : IRouter
    {
        private string[] urls;

        public LegacyRoute(params string[] targetUrls)
        {
            this.urls = targetUrls;
        }

        public Task RouteAsync(RouteContext context)
        {
            string requestedUrl = context.HttpContext.Request.Path
                .Value.TrimEnd('/');
            if (urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase))
            {
                context.Handler = async ctx => {
                    HttpResponse response = ctx.Response;
                    byte[] bytes = Encoding.ASCII.GetBytes($"URL: {requestedUrl}");
                    await response.Body.WriteAsync(bytes, 0, bytes.Length);
                };
            }
            return Task.CompletedTask;
        }

        public VirtualPathData GetVirtualPath(VirtualPathContext context)
        {
            return null;
        }
    }
}

LecagyRoute类实现了IRouter接口,但只定义了RouteAsync方法的代码,该方法用于处理传入的请求;我很快就会添加对传出 URL 的支持。

RouteAsync方法中只有几个语句,但是它们依赖于许多重要的 ASP.NET 类型来完成它们的工作。最好的起点是使用方法签名。

...
public async Task RouteAsync(RouteContext context) {
...

RouteAsync方法负责评估请求是否可以被处理,如果可以的话,它将管理流程直到生成发送回客户端的响应。这个过程是异步执行的,这就是RouteAsync方法返回Task的原因。

RouteAsync方法使用RouteContext参数调用,该参数提供对有关请求的所有已知内容的访问,并提供将响应发送回客户端所需的特性。RouteContext类在Microsoft.AspNetCore.Routing命名空间中定义,并定义了表16-4所示的三个属性。

表 16-4:RouteContext 类定义的三个属性

名称 描述
RouteData 此属性返回一个Microsoft.AspNetCore.Routing.RouteData对象。在编写依赖 MVC 特性的自定义路由时(如下一节所述),此对象用于定义控制器、action 方法和将用于处理请求的参数。
HttpContext 此属性返回一个Microsoft.AspNetCore.Http.HttpContext对象,它提供对 HTTP 请求的详细信息的访问以及生成 HTTP 响应的方法
Handler 此属性用于向路由系统提供将处理请求的RequestDelegate。如果RouteAsync方法没有设置此属性,则路由系统将继续通过应用程序配置中的路由集工作。

路由系统调用应用程序中每个路由的RouteAsync方法,并在每次调用后检查Handler属性的值。如果属性已设置为RequestDelegate,则路由为路由系统提供了一个可以处理请求的委托,并调用委托来生成响应。下面是RequestDelegate的签名,它在Microsoft.AspNetCore.Http命名空间中定义:

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Http {
    public delegate Task RequestDelegate(HttpContext context);
}

此委托接受HttpContext对象并返回将生成响应的Task。如果没有任何路由设置了Handler属性,则路由系统知道应用程序无法处理请求,并将生成【404 - Not Found】的响应。

考虑到这一点,RouteAsync方法的实现必须确定它是否能够处理请求,为此通常需要HttpContext。在本例中,我使用HttpContext.Request属性返回描述请求的Microsoft.AspNetCore.Http.HttpRequest对象。HttpRequest对象提供对有关请求的所有可用信息的访问,包括 headers、body 和请求来源的详细信息,但我感兴趣的是Path属性,因为它提供了客户端请求 URL 的详细信息。Path属性返回一个PathString字符串对象,它为合成和比较 URL 路径提供了有用的方法,但是我使用Value属性,因为它将 URL 的整个路径部分作为字符串,我可以将它与LegacyRoute构造函数接收到的一组受支持的 URL 进行比较。

...
string requestedUrl = context.HttpContext.Request.Path.Value.TrimEnd('/');
if (urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase)) {
...

我使用 URL 上的TrimEnd方法来删除尾斜杠(如果有),该斜杠可以由用户添加,也可以由《更改路由系统配置》一节中描述的AppendTrailingSlash配置选项添加。

如果请求路径是由LegacyRoute配置的支持的路径之一,则使用将生成响应的 lambda 函数设置Handler属性,如下所示:

...
context.Handler = async ctx => {
    HttpResponse response = ctx.Response;
    byte[] bytes = Encoding.ASCII.GetBytes($"URL: {requestedUrl}");
    await response.Body.WriteAsync(bytes, 0, bytes.Length);
};
...

HttpContext.Response属性返回一个HttpResponse对象,该对象可用于创建对客户端的响应,从而提供对将发送给客户端的 header 和内容的访问。我使用HttpResponse.Body.WriteAsync方法异步编写一个简单的 ASCII 字符串作为响应。这不是您在实际项目中会做的事情,但它允许我生成响应,而不必选择和渲染视图(尽管我在下一节中向您展示了如何让 MVC 为您完成此操作)。

设置了Handler属性后,路由系统就知道其对路由的搜索已经完成,并且可以调用委托来生成对客户端的响应。

应用自定义路由类

到目前为止,我用于创建路由的MapRoute扩展方法不支持使用自定义路由类。要应用我的LegacyRoute类,必须采取一种不同的方法,如清单16-15所示。

清单 16-15:UrlsAndRoutes 文件夹下的 Startup.cs 文件,应用自定义路由类

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options => {
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint));
                options.LowercaseUrls = true;
                options.AppendTrailingSlash = true;
            });
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.Routes.Add(new LegacyRoute(
                    "/articles/Windows_3.1_Overview.html",
                    "/old/.NET_1.0_Class_Library"));

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "out",
                    template: "outbound/{controller=Home}/{action=Index}");
            });
        }
    }
}

当使用自定义类时,必须使用路由集合上的Add方法来注册IRouter实现类。在示例中,LegacyRoute构造函数的参数是我希望自定义路由支持的遗留 URL。您可以通过启动应用程序和请求 /articles/Windows_3.1_Overview.html 来看到效果。自定义路由显示所请求的 URL,如图16-6所示。

图16-6 使用自定义路由

路由至 MVC 控制器

在匹配简单的 URL 字符串和使用由控制器、action 和 Razor 视图组成的 MVC 系统之间有很大的差距。幸运的是,在创建自定义路由时不必自己实现这个功能,因为 MVC 在幕后使用的类可以用来完成所有的繁重工作。为了准备使用 MVC 基础设施,我在 Controllers 文件夹中添加了一个名为 LegacyController.cs 的类文件,并使用它来定义如清单16-16所示的控制器。

清单 16-16:Controllers 文件夹下的 LegacyController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;

namespace UrlsAndRoutes.Controllers
{
    public class LegacyController : Controller
    {
        public ViewResult GetLegacyUrl(string legacyUrl)
            => View((object)legacyUrl);
    }
}

在此控制器中,GetLegacyUrl action 方法接受包含客户请求的遗留 URL 的参数。如果我在实际项目中实现此控制器,我将使用此方法检索所请求的文件。但实际上,我将在视图中显示 URL。

提示:对于清单16-16中的View方法,我将参数转换为object,其中一个重载版本的View方法将接受一个字符串,指定要渲染的视图的名称,如果没有强制转换,这将是 C# 编译器认为我想要的重载。为了避免这种情况,我将对象转换为object,以便明白地调用传递视图模型并使用默认视图的重载。我也可以通过使用视图名称和视图模型的重载来解决这个问题,但是如果可能的话,我不喜欢在操作方法和视图之间建立明确的关联。更多细节见第17章。

我创建了 Views/Legacy 文件夹,并添加了一个名为 GetLegacyUrl.cshtml 的视图,如清单16-17所示。该视图显示模型值,它将显示客户端请求的 URL。

清单 16-17:Views/Legacy 文件夹下的 GetLegacyUrl.cshtml 文件的内容

@model string
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <h2>GetLegacyURL</h2>
    The URL requested was: @Model
</body>
</html>

在清单16-18中,我更新了LegacyRoute类,以便它处理的 URL 被路由到Legacy控制器上的GetLegacyUrl操作。

清单 16-18:LegacyRoute.cs 文件,路由至控制器

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes.Infrastructure
{
    public class LegacyRoute : IRouter
    {
        private string[] urls;
        private IRouter mvcRoute;
        public LegacyRoute(IServiceProvider services, params string[] targetUrls)
        {
            this.urls = targetUrls;
            mvcRoute = services.GetRequiredService<MvcRouteHandler>();
        }
        public async Task RouteAsync(RouteContext context)
        {
            string requestedUrl = context.HttpContext.Request.Path
                .Value.TrimEnd('/');
            if (urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase))
            {
                context.RouteData.Values["controller"] = "Legacy";
                context.RouteData.Values["action"] = "GetLegacyUrl";
                context.RouteData.Values["legacyUrl"] = requestedUrl;
                await mvcRoute.RouteAsync(context);
            }
        }
        public VirtualPathData GetVirtualPath(VirtualPathContext context)
        {
            return null;
        }
    }
}

Microsoft.AspNetCore.Mvc.Internal.MvcRouteHandler类提供了将controlleraction段变量用于定位控制器类、执行 action 方法并将结果返回给客户端的机制。编写该类时,可以由提供controlleraction值以及任何其他所需的值(例如 action 方法参数)的自定义IRouter实现调用该类。

在清单16-18中,我创建了MvcRouteHandler类的一个新实例,将控制器类的定位任务委托给它。为此,我需要提供路由数据,如下所示:

...
context.RouteData.Values["controller"] = "Legacy";
context.RouteData.Values["action"] = "GetLegacyUrl";
context.RouteData.Values["legacyUrl"] = requestedUrl;
...

RouteContext.RouteData.Vales属性返回一个字典,用于向MvcRouteHandler类提供数据值。在默认的路由系统中,数据值是通过将 URL 模式应用于请求来创建的,但是在我的自定义路由类中,我对这些值进行了硬编码,从而使 Legacy 控制器上的 GetLegacyUrl action 始终成为目标。请求之间唯一的变化是 legacyUrl 数据值,它被设置为请求 URL,它将用作 action 方法接收到的同名参数。

清单16-18中的最后更改委托了查找和使用控制器类来处理请求的责任。

...
await mvcRoute.RouteAsync(context);
...

现在包含controlleractionlegacyUrl值的RouteContext对象被传递给MvcRouteHandler对象的RouteAsync方法,该方法负责对请求的任何进一步处理,包括设置处理程序属性。结果是LegacyRoute类可以专注于决定它将处理哪些 URL,而不会被直接处理控制器的细节所困扰。

在本例中执行工作的MvcRouteHandler对象必须作为服务被请求,我在第18章中对此进行了解释。为了为LegacyRoute构造函数提供创建MvcRouteHandler所需的IServiceProvider对象,我更新了定义路由的语句,以便在Startup类中为其提供对应用程序服务的访问,如清单16-19所示。

清单 16-19:UrlsAndRoutes 文件夹下的 Startup 类,提供对服务的访问

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options => {
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint));
                options.LowercaseUrls = true;
                options.AppendTrailingSlash = true;
            });
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.Routes.Add(new LegacyRoute(
                    app.ApplicationServices,
                    "/articles/Windows_3.1_Overview.html",
                    "/old/.NET_1.0_Class_Library"));

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "out",
                    template: "outbound/{controller=Home}/{action=Index}");
            });
        }
    }
}

如果启动应用程序并再次请求 /articles/Windows_3.1_Overview.html,您将看到简单的文本响应现在被视图的输出所替换,如图16-7所示。

图16-7 委托处理控制器和 action

生成传出 URL

为了支持生成传出 URL,我需要在LegacyRoute类中实现GetVirtualPath方法,如清单16-20所示。

清单 16-20:Infrastructure 文件夹下的 LegacyRoute.cs 文件,生成传出 URL

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes.Infrastructure
{
    public class LegacyRoute : IRouter
    {
        private string[] urls;
        private IRouter mvcRoute;
        public LegacyRoute(IServiceProvider services, params string[] targetUrls)
        {
            this.urls = targetUrls;
            mvcRoute = services.GetRequiredService<MvcRouteHandler>();
        }
        public async Task RouteAsync(RouteContext context)
        {
            string requestedUrl = context.HttpContext.Request.Path
                .Value.TrimEnd('/');
            if (urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase))
            {
                context.RouteData.Values["controller"] = "Legacy";
                context.RouteData.Values["action"] = "GetLegacyUrl";
                context.RouteData.Values["legacyUrl"] = requestedUrl;
                await mvcRoute.RouteAsync(context);
            }
        }
        public VirtualPathData GetVirtualPath(VirtualPathContext context)
        {
            if (context.Values.ContainsKey("legacyUrl"))
            {
                string url = context.Values["legacyUrl"] as string;
                if (urls.Contains(url))
                {
                    return new VirtualPathData(this, url);
                }
            }
            return null;
        }
    }
}

路由系统调用已在Startup类中定义的每个路由的GetVirtualPath方法,使每个路由都有机会生成应用程序所需的传出 URL。GetVirtualPath方法的参数是一个VirtualPathContext对象,它提供有关需要的 URL 的信息。表16-5描述了VirtualPathContext类的属性。

表 16-5VirtualPathContext类定义的属性

名称 描述
RouteName 此属性返回路由的名称
Values 此属性返回可用于段变量的所有值的字典,按名称索引
AmbientValues 此属性返回有助于生成 URL 但不会合并到结果中的值的字典。当您实现自己的路由类时,此字典通常是空的。
HttpContext 此属性返回一个HttpContext对象,它提供有关请求和正在为其准备的响应的信息。

在本例中,我使用Values属性获取一个名为legacyUrl的值,如果它与路由配置所支持的 URL 之一相匹配,则返回一个VirtualPathData对象,该对象向路由系统提供 URL 的详细信息。

...
return new VirtualPathData(this, url);
...

在清单16-21中,我更改了 Result.cshtml 视图,从而满足以自定义视图为目标的传出 URL。

清单 16-21:Views/Shared 文件夹下的 Result.cshtml 文件,生成传出 URL

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
    <a asp-route-legacyurl="/articles/Windows_3.1_Overview.html"
       class="btn btn-primary">
        This is an outgoing URL
    </a>
    <p>
        URL: @Url.Action(null, null,
            new { legacyurl = "/articles/Windows_3.1_Overview.html" })
    </p>
</body>
</html>

在本例中,我不需要为标签助手的传出路由指定控制器和 action,因为它们不在 URL 生成中使用。考虑到这一点,我省略了a元素中的asp-controllerasp-action标签助手属性。在生成 URL 时,出于同样的原因,我将Url.Action助手的前两个参数设置为null

如果运行应用程序,并检查默认 URL 响应中的 HTML,您将看到自定义路由类创建的 URL,如下:

<a class="btn btn-primary" href="/articles/windows_3.1_overview.html/">
    This is an outgoing URL
</a>
<p>URL: /articles/windows_3.1_overview.html/</p

追加到 URL 的尾斜杠是在 Startup.cs 文件中将AppendTrailingSlash配置选项设置为true的结果,重要的是传入路由匹配能够匹配已添加斜杠字符的 URL。

提示:如果您在 HTML 响应中看到的 URL 具有不同的格式,如 /?legacyurl=%2Farticles%2FWindows_3.1_Overview.html ,则您的自定义路由没有用于生成 URL,而是调用了应用程序中的其他路由。由于没有指定控制器或 action,因此将针对 Home 控制器上的Index action,并将legacyUrl值添加到 URL 查询字符串中。如果发生这种情况,请确保您已经记住在GetVirtualPath方法中将IsBound属性设置为true,并检查 Startup.cs 文件中的配置是否为LegacyRoute构造函数指定了正确的 URL,并且自定义路由在任何其他路由之前定义。

使用区域

ASP.NET Core MVC 支持将 Web 应用程序组织到各个区域(area),每个区域代表应用程序的一个功能部分,例如管理、计费、客户支持等。这在大型项目中很有用,在这个项目中,拥有一组用于所有控制器、视图和模型的文件夹可能变得很难管理。

每个 MVC 区域都有自己的文件夹结构,允许您将所有内容保持分离。这使得与应用程序的每个功能区域相关的项目元素变得更加明显,帮助多个开发人员在项目上工作而不相互冲突。区域主要由路由系统支持,这也是我在 URL 和路由中描述这个功能的原因。本节我将向您展示如何在 MVC 项目中设置和使用区域。

创建一个区域

创建区域需要在项目中添加文件夹。最顶层文件夹名为 Areas,里面则是您需要的每个区域的文件夹,它们中的每一个都包含自己的 Controllers、Views 和 Models 文件夹。本章中我将创建一个名为 Admin 的区域,这意味着创建表16-6中描述的文件夹集合。要准备示例项目,请创建表中显示的所有文件夹。

表 16-6:需要为区域准备的文件夹

名称 描述
Areas 此文件夹将包含 MVC 应用程序中的所有区域
Areas/Admin 此文件夹将包含 Admin 区域的类和视图
Areas/Admin/Controllers 此文件夹将包含 Admin 区域的控制器
Areas/Admin/Views 此文件夹将包含 Admin 区域的视图
Areas/Admin/Views/Home 此文件夹将包含 Admin 区域中的 Home 控制器的视图
Areas/Admin/Models 此文件夹将包含 Admin 区域的模型

译者注:我使用的 Visual Studio 15.8.1 版本中有自动添加区域的功能,早期版本有没有不得而知。在项目上点击鼠标右键,在弹出菜单中选择【添加】➤【区域】,在弹出窗口中输入 Admin,则会自动创建上述文件夹。

虽然每个区域都是单独使用的,但大多数 MVC 特性依赖于标准 C# 或 .NET 特性,如命名空间。为了让一个区域更容易使用,我首先添加了一个视图导入文件,它允许我在不包含命名空间的前提下在视图的区域中使用模型,并利用标签助手。我在 Areas/Admin/Views 文件夹中创建了一个名为 _ViewImports.cshtml 的视图导入文件,并添加了清单16-22中所示的语句。

清单 16-22:Areas/Admin/Views 文件夹下的 _ViewImports.cshtml 文件的内容

@using UrlsAndRoutes.Areas.Admin.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

创建一个区域路由

要使用区域,必须向 Startup.cs 文件添加一个路由,其中包含一个area段变量,如清单16-23所示。

清单 16-23:UrlsAndRoutes 文件夹下的 Startup.cs 文件,为区域添加一个路由

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options => {
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint));
                options.LowercaseUrls = true;
                options.AppendTrailingSlash = true;
            });
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "areas",
                    template: "{area:exists}/{controller=Home}/{action=Index}");

                routes.Routes.Add(new LegacyRoute(
                    app.ApplicationServices,
                    "/articles/Windows_3.1_Overview.html",
                    "/old/.NET_1.0_Class_Library"));

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "out",
                    template: "outbound/{controller=Home}/{action=Index}");
            });
        }
    }
}

area段变量用于匹配指定区域中的目标控制器的 URL。我在清单中遵循了标准的 URL 模式,但是您可以将area段添加到任何需要的模式中。添加对区域支持的路由应该出现在不太具体的路由之前,以确保 URL 是正确匹配的。exists约束用于确保请求只匹配在应用程序中定义的区域。

填充区域

您可以在某个区域创建控制器、视图和模型,就像在 MVC 应用程序的主要部分中一样。为了创建一个模型,我右键单击了 Areas/Admin/Models 文件夹,从弹出菜单中选择【添加】➤【类】,并创建了一个名为 Person.cs 的类文件,其内容如清单16-24所示。

清单 16-24:Areas/Admin/Models 文件夹下的 Person.cs 文件的内容

namespace UrlsAndRoutes.Areas.Admin.Models
{
    public class Person
    {
        public string Name { get; set; }
        public string City { get; set; }
    }
}

要创建控制器,我右键单击 Areas/Admin/Controllers 文件夹,在弹出菜单中选择【添加】➤【类】,并创建了一个名为 HomeController.cs 的文件,用于定义清单 16-25 所示的控制器。

清单 16-25:Areas/Admin/Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Areas.Admin.Models;

namespace UrlsAndRoutes.Areas.Admin.Controllers
{
    [Area("Admin")]
    public class HomeController : Controller
    {
        private Person[] data = new Person[] {
            new Person { Name = "Alice", City = "London" },
            new Person { Name = "Bob", City = "Paris" },
            new Person { Name = "Joe", City = "New York" }
        };

        public ViewResult Index() => View(data);
    }
}

新的控制器是完全标准的,除了一个方面。若要将控制器与区域相关联,必须将Area特性应用于类。

...
[Area("Admin")]
public class HomeController : Controller {
...

如果没有Area特性,即使控制器是在应用程序的主要部分中定义的,它也不是区域的一部分。忽略Area特性可能会导致奇怪的结果。如果您在使用区域时没有得到预期的结果,这是第一件要检查的事情。

提示:如果使用特性设置路由,如第15章所述,则可以在Route特性参数中使用[area]令牌来引用由Area特性指定的区域:[Route("[area]/app/[controller]/actions/[action]/{id:weekday?}")]

我添加的最后一项是 Areas/Admin/Views/Home 文件夹中名为 Index.cshtml 的 Razor 视图。我使用这个文件来定义清单16-26所示的视图。

清单 16-26:Areas/Admin/Views/Home 文件夹下的 Index.cshtml 文件的内容

@model Person[]
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Areas</title>
    <link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Name</th><th>City</th></tr>
        @foreach (Person p in Model)
        {
            <tr><td>@p.Name</td><td>@p.City</td></tr>
        }
    </table>
</body>
</html>

这个视图的模型是一个Person对象数组。我可以引用Person类型,而不需要命名空间,因为我在清单16-22中创建了视图导入文件。运行应用程序并请求 /Admin URL 来测试区域,这将产生如图16-8所示的结果。

图16-8 使用区域


理解区域对MVC应用程序的影响

理解区域对应用程序其余部分的影响是很重要的。我创建了一个名为 Admin 的区域,但是应用程序的主要部分也有一个 Admin 控制器。在创建区域之前,对 /Admin 的请求将以应用程序主要部分中 Admin 控制器上的Index action 为目标;现在,它将针对 Admin 区域中 Home 控制器上的Index action(区域根为controlleraction段变量提供默认值)。这种更改可能导致意外行为,使用区域的最佳方法是将它们的使用合并到项目的初始控制器命名方案中。如果您必须返回并添加区域到一个既定的应用程序,那么必须仔细考虑对您的路由的影响。


为区域内的 Action 生成链接

您不需要采取任何特殊步骤来创建链接,以引用当前请求所涉及的同一个 MVC 区域中的 action。MVC 检测到请求与特定区域相关,并确保出站 URL 生成只为该区域定义的路由之间寻找匹配。作为例子,清单16-27显示了在 Areas/Admin/Views/Home 文件夹中添加一个元素到 Index.cshtml 文件中。

清单 16-27:Areas/Admin/Views/Home 文件夹下的 Index.cshtml 文件,添加锚

@model Person[]
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Areas</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Name</th><th>City</th></tr>
        @foreach (Person p in Model)
        {
            <tr><td>@p.Name</td><td>@p.City</td></tr>
        }
    </table>
    <a asp-action="Index" asp-controller="Home">Link</a>
</body>
</html>

如果运行应用程序并请求 /admin URL,您将看到响应包含以下元素:

<a href="/admin/">Link</a>

路由系统选择了区域路由以生成传出链接,并考虑到controlleraction段变量可用的默认值。

您必须为路由系统提供一个用于区域段值,以便创建指向不同区域或应用程序主体部分中的 action 的链接,如清单16-28所示。

清单 16-28:Areas/Admin/Views/Home 文件夹下的 Index.cshtml 文件,针对不同的区域

@model Person[]
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Areas</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Name</th><th>City</th></tr>
        @foreach (Person p in Model)
        {
            <tr><td>@p.Name</td><td>@p.City</td></tr>
        }
    </table>
    <a asp-action="Index" asp-controller="Home">Link</a>
    <a asp-action="Index" asp-controller="Home" asp-route-area="">Link</a>
</body>
</html>

asp-route-area属性设置区域段变量的值。在本例中,属性设置为空字符串以指定应用程序的主要部分,并生成以下 HTML 元素:

<a href="/">Link</a>

如果控制器中有多个区域并希望路由到它们,则使用区域名称代替空字符串。

URL 架构最佳实践

在所有这些之后,您可能会想知道在设计自己的 URL 架构时从何处开始。您可以只接受默认架构,但是,对架构进行一些思考是有好处的。近年来,应用程序的 URL 设计越来越受到重视,出现了一些重要的设计原则。如果遵循这些设计模式,将提高应用程序的可用性、兼容性和搜索引擎排名。

让您的 URL 变得干净和人性化

用户注意到应用程序中的 URL。回想上一次尝试向某人发送 Amazon URL。下面是这本书的早期版本的 URL:

https://www.amazon.com/Pro-ASP-NET-Core-ADAM-FREEMAN/dp/1484203984

通过电子邮件发送这样一个网址已经够糟糕的了,试着通过电话来阅读就更糟糕了。当我最近需要这样做的时候,最后引用了ISBN的号码,并要求来电者自己查找。如果我能用这样的网址访问这本书,那就太好了:

http://www.amazon.com/books/pro-aspnet-mvc6-framework

这是我可以在电话上读出来的那种 URL,它看起来不像是我在写电子邮件的时候把什么东西掉到键盘上了。

注意:要说清楚的是,我对亚马逊只有最高的敬意,亚马逊卖的书比其他人加起来还要多。我知道一个事实,亚马逊团队的每一位成员都是一个非常聪明和美丽的人。他们中没有一个人会因为批评他们的 URL 格式而停止卖我的书。我爱死了。我很喜欢。我只是希望他们能修好他们的网址。

以下是一些制作友好 URL 的简单指南:

  • 设计 URL 来描述它们的内容,而不是描述应用程序的实现细节。使用 /Articles/AnnualReport 比 /Website_v2/CachedContentServer/FromCache/AnnualReport 更好。
  • 使用内容标题而不是 ID 号。使用 /Articles/AnnualReport 比 Articles/2392 更好。如果必须使用 ID 号(以区分具有相同标题的项或避免按标题查找项所需的额外数据库查询),则同时使用这两种方法(/Articles/2392/AnnualReport)。输入需要更长的时间,但对人来说更有意义,并且提高了搜索引擎的排名。您的应用程序可以忽略标题并显示与 ID 匹配的项。
  • 不要对 HTML 页面使用文件扩展名(如 .aspx 或 .mvc),而是将它们用于专门的文件类型(如 .jpg、.pdf 和 .zip)。如果适当地设置 MIME 类型,Web 浏览器并不关心文件扩展名,但人们仍然希望 PDF 文件以 .pdf 结尾。
  • 创建一种层次感(例如,/Products/Menswear/Shirts/Red),这样访问者就可以猜测父类别的 URL。
  • 不区分大小写(有人可能希望从打印页面中键入 URL)。默认情况下,ASP.NET Core 路由系统不区分大小写。
  • 避免符号、代码和字符序列。如果你想要一个分隔词,使用破折号(如 /my-great-article)。下划线是不友好的,URL 编码中的空格是奇怪的(/my+great+article)或恶心的(/my%20great%20article)。
  • 不要更改 URL。断开的链接等于丢失的业务。当您更改 URL 时,继续通过重定向尽可能长时间支持旧的 URL 架构。
  • 保持一致性。在整个应用程序中采用一种 URL 格式。

URL 应该是短的、容易键入的、可修改的(可人工编辑)和持久的,并且应该可视化站点结构。可用性大师 Jakob Nielsen 在 www.useit.com/alertbox/990321.html 上对这个主题进行了扩展。网络的发明者伯纳斯-李也提供了类似的建议(参考www.w3.org/Provider/Style/URI)。

GET 和 POST:选择正确的

经验法则是,GET 请求应用于所有只读信息检索,而 POST 请求则应用于任何更改应用程序状态的操作。在 standards-compliance 术语中,GET 请求用于安全交互(除了信息检索没有副作用),而 POST 请求用于不安全的交互(做出决定或更改某些内容)。这些约定是由万维网联盟(W3C)制定的www.w3.org/Protocols/rfc2616/rfc2616-sec9.html

GET 请求是可寻址的:所有信息都包含在 URL 中,因此可以对这些地址进行书签和链接。

不要将 GET 请求用于做改变状态的操作。在 2005 年 Google Web 加速器向公众发布时,许多 web 开发人员就很难理解这一点。此应用程序预取从每个页面链接的所有内容,这在 HTTP 中是合法的,因为 GET 请求应该是安全的。不幸的是,许多 web 开发人员忽略了 HTTP 约定,在他们的应用程序中放置了简单的【删除项目】或【添加到购物车】的链接。

一家公司认为其内容管理系统是一再遭到恶意攻击的目标,因为所有的内容都保持删除状态。该公司后来发现,一个搜索引擎爬虫偶然发现了一个管理页面的网址,并且正在爬行所有的删除链接。身份验证可能会保护您不受此影响,但它不会保护您不受 web 加速器的影响。

总结

在本章中,我向您展示了路由系统的高级功能,向您展示了如何生成传出链接和 URL,以及如何定制路由系统。在此过程中,我介绍了区域的概念,并阐述了如何创建有用和有意义的 URL 架构的观点。在下一章中,我将转到控制器和 action,它们是ASP.NET Core MVC的核心。我将详细解释这些方法的工作原理,并向您展示如何使用它们在应用程序中获得最佳效果。

;

© 2018 - IOT小分队文章发布系统 v0.3